#version 330

in vec2 fs_Texcoord;

uniform sampler2D u_depthTX;  // Set as the depth buffer texture to avoid unpacking
uniform sampler2D u_normalTX;
uniform sampler2D u_miscTX;
uniform sampler2D u_lastMiscTX;
uniform sampler2D u_outTX;
uniform sampler2D u_edgeTX;
uniform sampler2D u_largeHorizontalBlurTX;
uniform sampler2D u_smallHorizontalBlurTX;
uniform sampler2D u_randomTX;

uniform float u_Near;
uniform float u_Far;

uniform mat4 u_invPersp;

uniform int u_screenWidth;
uniform int u_screenHeight;

out vec3 out_Color;

const float focusDepthNear = .25f;
const float focusDepthFar = .45f;
const float unfocusDepthFar = .55f;
const float unfocusDepthNear = .2f;

#define BLUR_MULT .75f

vec2 unpack4x8To2Float(in vec4 val) {
	const vec2 unshift = vec2(1.0f/256.0f, 1.0f);
	return vec2(dot(val.xy, unshift), dot(val.zw, unshift));
}

// Code from : http://aras-p.info/texts/CompactNormalStorage.html
vec3 unpackNormal(in vec2 encNorm) {
	if(encNorm.x < .000001f && encNorm.y < .00000001)
		return vec3(.0f, .0f, .0f);

	encNorm = encNorm*4.0f - 2.0f;
	float f = dot(encNorm, encNorm);
	float g = sqrt(1.0f -.25f*f);
	return vec3(encNorm*g,1.0f - .5f*f);
}

vec3 getVSPosition(in vec2 textureCoordinate) {
	vec4 sPos = vec4(2.0f*textureCoordinate.x - 1.0f, textureCoordinate.y * 2.0f - 1.0f, 2.0*texture(u_depthTX, textureCoordinate).x - 1.0f, 1.0f);
	sPos = u_invPersp * sPos;
	return sPos.xyz/sPos.w;
}

vec3 getVSPosition(in vec2 textureCoordinate, in float depth) {
	vec4 sPos = vec4(2.0f*textureCoordinate.x - 1.0f, textureCoordinate.y * 2.0f - 1.0f, 2.0*depth - 1.0f, 1.0f);
	sPos = u_invPersp * sPos;
	return sPos.xyz/sPos.w;
}

//Get a random scalar given a screen-space texture coordinate
//Fetches from a random texture
float getRandomScalar(in vec2 texcoords) {
	ivec2 sz = textureSize(u_randomTX,0);
	return texture(u_randomTX,vec2(texcoords.s*u_screenWidth/sz.x,
		texcoords.t*u_screenHeight/sz.y)).r;
}

#define FILTER_NUM 9
#define LINEAR_NUM 5
const float boxWeight[FILTER_NUM] = float[] (
	0.0751f,	0.1238f,	0.0751f, 
    0.1238f,	0.2042f,	0.1238f, 
    0.0751f,	0.1238f,	0.0751f	);
	
const ivec2 filterIndex[FILTER_NUM] = ivec2[] (
	vec2(-1,-1), vec2(-1,0), vec2(-1,1),
	vec2(0,-1),  vec2(0,0),  vec2(0,1),
	vec2(1,-1),  vec2(1,0),  vec2(1,1));
	
const ivec2 box[9] = ivec2[] (
	ivec2(-2,-2), ivec2(-2,0), ivec2(-2,2),
	ivec2(2,-2),  ivec2(2,0),  ivec2(2,2),
	ivec2(0,-2),  ivec2(0,0),  ivec2(0,2));
	
const ivec2 horizontalOffset[LINEAR_NUM] = ivec2[] (
	vec2(-2, 0), vec2(-1,0), vec2(0,0), vec2(1,0), vec2(2,0));
	
const ivec2 verticalOffset[LINEAR_NUM] = ivec2[] (
	vec2(0, -2), vec2(0,-1), vec2(0,0), vec2(0,1), vec2(0,2));

float linearizeDepth(float exp_depth) {
    return	(2 * u_Near) / (u_Far + u_Near -  exp_depth * (u_Far - u_Near)); 
}

const float distance_factor = 1.0f;
const float occlusion_weight = 2.0f;
//Estimate occlusion based on a point and a sampled occluder
//Design this function based on specified constraints
float gatherOcclusion( vec3 pt_normal,
	vec3 pt_position,
	vec3 occluder_normal,
	vec3 occluder_position) {
	
	// 0 = no occlusion, 1 = fully occluded
	
	
	// Determine level of co-planarity - goes from 0 to 1
	float planarityFactor = -dot(pt_normal, occluder_normal)/2 + .5;
	
	// Determine distance between two points, points far away should lead to low amounts of occlusion
	// Goes from 0 to 1
	float distanceFactor = exp(-distance(pt_position,occluder_position)/distance_factor);
	
	// Closeness of occluding point to normal factor
	float ptNormalFactor = max(0.0,dot(pt_normal,normalize(occluder_position-pt_position)));
	
	// Safe to return values over 1, because the main function clamps
	return occlusion_weight*planarityFactor*(distanceFactor*ptNormalFactor);
}

//Disk of samples for the SS sampling
#define NUM_SS_SAMPLES 16
vec2 poissonDisk[NUM_SS_SAMPLES] = vec2[]( 
    vec2( -0.94201624, -0.39906216 ), 
	vec2( 0.94558609, -0.76890725 ), 
	vec2( -0.094184101, -0.92938870 ), 
	vec2( 0.34495938, 0.29387760 ), 
	vec2( -0.91588581, 0.45771432 ), 
	vec2( -0.81544232, -0.87912464 ), 
	vec2( -0.38277543, 0.27676845 ), 
	vec2( 0.97484398, 0.75648379 ), 
	vec2( 0.44323325, -0.97511554 ), 
	vec2( 0.53742981, -0.47373420 ), 
	vec2( -0.26496911, -0.41893023 ), 
	vec2( 0.79197514, 0.19090188 ), 
	vec2( -0.24188840, 0.99706507 ), 
	vec2( -0.81409955, 0.91437590 ), 
	vec2( 0.19984126, 0.78641367 ), 
	vec2( 0.14383161, -0.14100790)
);

const float SS_RADIUS = 0.02f;
const float PI = 3.14159f;
float occlusionWithPoissonSSSamples(vec2 texcoord, 
	vec3 position,
    vec3 normal, float depth) {
    
    float rotationAngle = getRandomScalar(texcoord) * 2 * PI;
    mat2 rotationMatrix = mat2(vec2(cos(rotationAngle),sin(rotationAngle)),vec2(-sin(rotationAngle),cos(rotationAngle)));
    
    float occlusionSum = 0;
    
    vec2 sTexcoord;

    float depthFactor = 3.0f*(linearizeDepth(texture(u_depthTX, fs_Texcoord).x) + .1f)/1.1f;
    depthFactor *= depthFactor;
    for(int i = 0; i < NUM_SS_SAMPLES; i++) {
		sTexcoord = texcoord + SS_RADIUS*rotationMatrix*poissonDisk[i]/(depthFactor);
		float sampleDepth = texture(u_depthTX, sTexcoord).x;
		
		occlusionSum += gatherOcclusion(normal, position, 
			unpackNormal(unpack4x8To2Float(texture(u_normalTX,sTexcoord))),
			getVSPosition(sTexcoord, sampleDepth))*inversesqrt(depthFactor);
    }
    
	return occlusionSum/NUM_SS_SAMPLES;
}

void main(void) {

	vec3 color = texture(u_outTX, fs_Texcoord).xyz;
	vec4 miscRead = texture(u_miscTX, fs_Texcoord);
	float depth = texture(u_depthTX, fs_Texcoord).x;
	vec2 offset = vec2(.0f, 1.0f/u_screenHeight);

// Post-process edge blurring
#ifdef PP_EDGE_BLUR
	if(texture(u_edgeTX, fs_Texcoord).x > .5f) {
		color = vec3(0.0f,.0f,.0f);
		for(int i = 0; i < 9; ++i)
			color += boxWeight[i]*textureOffset(u_outTX, fs_Texcoord, filterIndex[i]).xyz;
	}
#endif

// Ambient occlusion
#ifdef PP_AMBIENT_OCCLUSION
	float occlusionFactor;
	if(miscRead.z > .5f/255.0f)
		occlusionFactor =  occlusionWithPoissonSSSamples(fs_Texcoord, 
			getVSPosition(fs_Texcoord),
			unpackNormal(unpack4x8To2Float(texture(u_normalTX,fs_Texcoord))),
			texture(u_depthTX, fs_Texcoord).x);
	else
		occlusionFactor = .0f;
			
	color = color*(.2f + .8f*(1.0 - clamp(occlusionFactor, .0f, 1.0f)));
	//float depthFactor = (linearizeDepth(texture(u_depthTX, fs_Texcoord).x) + .1f)/1.1f;
	//color = vec3(depthFactor*depthFactor,depthFactor*depthFactor,depthFactor*depthFactor);
#endif

// Post-process depth-of-field - 4 different possibilites (MAX, MIN, DoF, None)
#ifdef PP_DEPTH_OF_FIELD
	// Sample nearby depths to determine which depth to use for DoF
	float maxDepth = depth, minDepth = depth;
	float readDepth = textureOffset(u_depthTX, fs_Texcoord, ivec2(-3,-3)).x;
	maxDepth = max(maxDepth, readDepth);
	minDepth = min(minDepth, readDepth);
	readDepth = textureOffset(u_depthTX, fs_Texcoord, ivec2(-3,3)).x;
	maxDepth = max(maxDepth, readDepth);
	minDepth = min(minDepth, readDepth);
	readDepth = textureOffset(u_depthTX, fs_Texcoord, ivec2(3,3)).x;
	maxDepth = max(maxDepth, readDepth);
	minDepth = min(minDepth, readDepth);
	readDepth = textureOffset(u_depthTX, fs_Texcoord, ivec2(3,-3)).x;
	maxDepth = max(maxDepth, readDepth);
	minDepth = min(minDepth, readDepth);
	
	// Find linear min/max depth
	maxDepth = linearizeDepth(maxDepth);
	minDepth = linearizeDepth(minDepth);
	
	// Find the amount of unfocus
	minDepth = (focusDepthNear - minDepth)/(focusDepthNear - unfocusDepthNear);
	maxDepth = (maxDepth - focusDepthFar)/(unfocusDepthFar - focusDepthFar);
	
	// Out of focus
	if(minDepth > 0 || maxDepth > 0) {
		vec3 blurRead;
		float usedDepth;
		float d = 1 - linearizeDepth(depth);
		if(minDepth > maxDepth) {		
			blurRead = 
				texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*12*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*10*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*8*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*6*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*4*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*2*offset).xyz +
				color +
				texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*2*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*4*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*6*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*8*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*10*offset).xyz +
				texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*12*offset).xyz;
				
			blurRead /= 13.0f;
			
			usedDepth = clamp(minDepth, 0, 1);
		}
		else {
			blurRead = 
				texture(u_smallHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*6*offset).xyz +
				texture(u_smallHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*4*offset).xyz +
				texture(u_smallHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*2*offset).xyz +
				color +
				texture(u_smallHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*2*offset).xyz +
				texture(u_smallHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*4*offset).xyz +
				texture(u_smallHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*6*offset).xyz;
				
			blurRead /= 7.0f;
				
			usedDepth = clamp(maxDepth, 0, 1);
		}
		color = mix(color, blurRead, usedDepth);		
	}
#else
#ifdef PP_MIN_DEPTH
	float d = 1 - linearizeDepth(depth);
	vec3 blurRead = vec3(.0f,.0f,.0f);
	blurRead +=
		texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*12*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*10*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*8*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*6*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*4*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*2*offset).xyz +
		color +
		texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*2*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*4*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*6*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*8*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*10*offset).xyz +
		texture(u_largeHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*12*offset).xyz;
	blurRead *= (1.0f/13.0f);
	color = blurRead;
#endif
#ifdef PP_MAX_DEPTH
	float d = 1 - linearizeDepth(depth);
	vec3 blurRead = vec3(.0f,.0f,.0f);
	blurRead +=
		texture(u_smallHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*6*offset).xyz +
		texture(u_smallHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*4*offset).xyz +
		texture(u_smallHorizontalBlurTX, fs_Texcoord - d*BLUR_MULT*2*offset).xyz +
		color +
		texture(u_smallHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*2*offset).xyz +
		texture(u_smallHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*4*offset).xyz +
		texture(u_smallHorizontalBlurTX, fs_Texcoord + d*BLUR_MULT*6*offset).xyz;
	blurRead *= (1.0f/7.0f);
	color = blurRead;
#endif
#endif

// Post-process motion blur
#ifdef PP_MOTION_BLUR
	vec4 lastMiscRead = texture(u_lastMiscTX, fs_Texcoord);
	if(miscRead.w > 1.5f/255.0f || lastMiscRead.w > 1.5f/255.0f) {
		// Technique from GPU Gems 3, Chapter 27
		
		vec2 velocity = 2.5f*(miscRead.xy - .5f)/(depth);
		vec3 motionBlur = color;
		velocity.x /= u_screenWidth;
		velocity.y /= u_screenHeight;
		vec2 texCoord = fs_Texcoord - velocity;
		int count = 0;
		for(int i = 0; i < 8; ++i, texCoord -= velocity) {
			// Only sample points from the same object
			float objRead = texture(u_miscTX, texCoord).z;
			//if(abs(objRead - miscRead.z) < .1f*(1.0f/256.0f) || abs(objRead - lastMiscRead.z) < .1f*(1.0f/256.0f)) {
				++count;
				motionBlur += texture(u_outTX, texCoord).xyz;
			//}
		}
		motionBlur /= count + 1;
		color = .5f*motionBlur + .5f*color;
		
		
		// Just do a box blur then weight scaled by the total velocity
		// Pixels won't be directionally blurred, but faster moving
		// objects will at least be blurred more
		/*
		vec3 blurColor = vec3(0.0f, 0.0f, 0.0f);
		for(int i = 0; i < 9; ++i)
			blurColor += textureOffset(u_outTX, fs_Texcoord, box[i]).xyz;
		blurColor /= 9.0f;
			
		color = mix(color, blurColor, inversesqrt(2.0f)*length(.5f*(miscRead.xy - .5f));
		*/
	}
#endif

	out_Color = color;
}